/* eslint-disable */
/*
 * jQuery Mobile Framework : "mouse" plugin
 * Copyright (c) jQuery Project
 * Dual licensed under the MIT or GPL Version 2 licenses.
 * http://jquery.org/license
 */

// This plugin is an experiment for abstracting away the touch and mouse
// events so that developers don't have to worry about which method of input
// the device their document is loaded on supports.
//
// The idea here is to allow the developer to register listeners for the
// basic mouse events, such as mousedown, mousemove, mouseup, and click,
// and the plugin will take care of registering the correct listeners
// behind the scenes to invoke the listener at the fastest possible time
// for that device, while still retaining the order of event firing in
// the traditional mouse environment, should multiple handlers be registered
// on the same element for different events.
//
// The current version exposes the following virtual events to jQuery bind methods:
// "vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel"
import $ from "jquery";

(function ($, window, document, undefined) {
    if (typeof $ === "undefined") {
        return;
    }

    var dataPropertyName = "virtualMouseBindings",
        touchTargetPropertyName = "virtualTouchID",
        virtualEventNames =
            "vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel".split(
                " ",
            ),
        touchEventProps = "clientX clientY pageX pageY screenX screenY".split(
            " ",
        ),
        mouseHookProps = $.event.mouseHooks ? $.event.mouseHooks.props : [],
        mouseEventProps = $.event.props.concat(mouseHookProps),
        activeDocHandlers = {},
        resetTimerID = 0,
        startX = 0,
        startY = 0,
        didScroll = false,
        clickBlockList = [],
        blockMouseTriggers = false,
        blockTouchTriggers = false,
        eventCaptureSupported = "addEventListener" in document,
        $document = $(document),
        nextTouchID = 1,
        lastTouchID = 0;

    $.vmouse = {
        moveDistanceThreshold: 10,
        clickDistanceThreshold: 10,
        resetTimerDuration: 1500,
    };

    function getNativeEvent(event) {
        while (event && typeof event.originalEvent !== "undefined") {
            event = event.originalEvent;
        }
        return event;
    }

    function createVirtualEvent(event, eventType) {
        var t = event.type,
            oe,
            props,
            ne,
            prop,
            ct,
            touch,
            i,
            j,
            len;

        event = $.Event(event);
        event.type = eventType;

        oe = event.originalEvent;
        props = $.event.props;

        // addresses separation of $.event.props in to $.event.mouseHook.props and Issue 3280
        // https://github.com/jquery/jquery-mobile/issues/3280
        if (t.search(/mouse/) > -1) {
            props = mouseEventProps;
        }

        // copy original event properties over to the new event
        // this would happen if we could call $.event.fix instead of $.Event
        // but we don't have a way to force an event to be fixed multiple times
        if (oe) {
            for (i = props.length, prop; i; ) {
                prop = props[--i];
                event[prop] = oe[prop];
            }
        }

        // make sure that if the mouse and click virtual events are generated
        // without a .which one is defined
        if (t.search(/mouse(down|up)|click/) > -1 && !event.which) {
            event.which = 1;
        }

        if (t.search(/^touch/) !== -1) {
            ne = getNativeEvent(oe);
            t = ne.touches;
            ct = ne.changedTouches;
            touch = t && t.length ? t[0] : ct && ct.length ? ct[0] : undefined;

            if (touch) {
                for (j = 0, len = touchEventProps.length; j < len; j++) {
                    prop = touchEventProps[j];
                    event[prop] = touch[prop];
                }
            }
        }

        return event;
    }

    function getVirtualBindingFlags(element) {
        var flags = {},
            b,
            k;

        while (element) {
            b = $.data(element, dataPropertyName);

            for (k in b) {
                if (b[k]) {
                    flags[k] = flags.hasVirtualBinding = true;
                }
            }
            element = element.parentNode;
        }
        return flags;
    }

    function getClosestElementWithVirtualBinding(element, eventType) {
        var b;
        while (element) {
            b = $.data(element, dataPropertyName);

            if (b && (!eventType || b[eventType])) {
                return element;
            }
            element = element.parentNode;
        }
        return null;
    }

    function enableTouchBindings() {
        blockTouchTriggers = false;
    }

    function disableTouchBindings() {
        blockTouchTriggers = true;
    }

    function enableMouseBindings() {
        lastTouchID = 0;
        clickBlockList.length = 0;
        blockMouseTriggers = false;

        // When mouse bindings are enabled, our
        // touch bindings are disabled.
        disableTouchBindings();
    }

    function disableMouseBindings() {
        // When mouse bindings are disabled, our
        // touch bindings are enabled.
        enableTouchBindings();
    }

    function startResetTimer() {
        clearResetTimer();
        resetTimerID = setTimeout(function () {
            resetTimerID = 0;
            enableMouseBindings();
        }, $.vmouse.resetTimerDuration);
    }

    function clearResetTimer() {
        if (resetTimerID) {
            clearTimeout(resetTimerID);
            resetTimerID = 0;
        }
    }

    function triggerVirtualEvent(eventType, event, flags) {
        var ve;

        if (
            (flags && flags[eventType]) ||
            (!flags &&
                getClosestElementWithVirtualBinding(event.target, eventType))
        ) {
            ve = createVirtualEvent(event, eventType);

            $(event.target).trigger(ve);
        }

        return ve;
    }

    function mouseEventCallback(event) {
        var touchID = $.data(event.target, touchTargetPropertyName);

        if (!blockMouseTriggers && (!lastTouchID || lastTouchID !== touchID)) {
            var ve = triggerVirtualEvent("v" + event.type, event);
            if (ve) {
                if (ve.isDefaultPrevented()) {
                    event.preventDefault();
                }
                if (ve.isPropagationStopped()) {
                    event.stopPropagation();
                }
                if (ve.isImmediatePropagationStopped()) {
                    event.stopImmediatePropagation();
                }
            }
        }
    }

    function handleTouchStart(event) {
        var touches = getNativeEvent(event).touches,
            target,
            flags;

        if (touches && touches.length === 1) {
            target = event.target;
            flags = getVirtualBindingFlags(target);

            if (flags.hasVirtualBinding) {
                lastTouchID = nextTouchID++;
                $.data(target, touchTargetPropertyName, lastTouchID);

                clearResetTimer();

                disableMouseBindings();
                didScroll = false;

                var t = getNativeEvent(event).touches[0];
                startX = t.pageX;
                startY = t.pageY;

                triggerVirtualEvent("vmouseover", event, flags);
                triggerVirtualEvent("vmousedown", event, flags);
            }
        }
    }

    function handleScroll(event) {
        if (blockTouchTriggers) {
            return;
        }

        if (!didScroll) {
            triggerVirtualEvent(
                "vmousecancel",
                event,
                getVirtualBindingFlags(event.target),
            );
        }

        didScroll = true;
        startResetTimer();
    }

    function handleTouchMove(event) {
        if (blockTouchTriggers) {
            return;
        }

        var t = getNativeEvent(event).touches[0],
            didCancel = didScroll,
            moveThreshold = $.vmouse.moveDistanceThreshold,
            didScroll =
                didScroll ||
                Math.abs(t.pageX - startX) > moveThreshold ||
                Math.abs(t.pageY - startY) > moveThreshold,
            flags = getVirtualBindingFlags(event.target);

        if (didScroll && !didCancel) {
            triggerVirtualEvent("vmousecancel", event, flags);
        }

        triggerVirtualEvent("vmousemove", event, flags);
        startResetTimer();
    }

    function handleTouchEnd(event) {
        if (blockTouchTriggers) {
            return;
        }

        disableTouchBindings();

        var flags = getVirtualBindingFlags(event.target),
            t;
        triggerVirtualEvent("vmouseup", event, flags);

        if (!didScroll) {
            var ve = triggerVirtualEvent("vclick", event, flags);
            if (ve && ve.isDefaultPrevented()) {
                // The target of the mouse events that follow the touchend
                // event don't necessarily match the target used during the
                // touch. This means we need to rely on coordinates for blocking
                // any click that is generated.
                t = getNativeEvent(event).changedTouches[0];
                clickBlockList.push({
                    touchID: lastTouchID,
                    x: t.clientX,
                    y: t.clientY,
                });

                // Prevent any mouse events that follow from triggering
                // virtual event notifications.
                blockMouseTriggers = true;
            }
        }
        triggerVirtualEvent("vmouseout", event, flags);
        didScroll = false;

        startResetTimer();
    }

    function hasVirtualBindings(ele) {
        var bindings = $.data(ele, dataPropertyName),
            k;

        if (bindings) {
            for (k in bindings) {
                if (bindings[k]) {
                    return true;
                }
            }
        }
        return false;
    }

    function dummyMouseHandler() {}

    function getSpecialEventObject(eventType) {
        var realType = eventType.substr(1);

        return {
            setup: function (data, namespace) {
                // If this is the first virtual mouse binding for this element,
                // add a bindings object to its data.

                if (!hasVirtualBindings(this)) {
                    $.data(this, dataPropertyName, {});
                }

                // If setup is called, we know it is the first binding for this
                // eventType, so initialize the count for the eventType to zero.
                var bindings = $.data(this, dataPropertyName);
                bindings[eventType] = true;

                // If this is the first virtual mouse event for this type,
                // register a global handler on the document.

                activeDocHandlers[eventType] =
                    (activeDocHandlers[eventType] || 0) + 1;

                if (activeDocHandlers[eventType] === 1) {
                    $document.bind(realType, mouseEventCallback);
                }

                // Some browsers, like Opera Mini, won't dispatch mouse/click events
                // for elements unless they actually have handlers registered on them.
                // To get around this, we register dummy handlers on the elements.

                $(this).bind(realType, dummyMouseHandler);

                // For now, if event capture is not supported, we rely on mouse handlers.
                if (eventCaptureSupported) {
                    // If this is the first virtual mouse binding for the document,
                    // register our touchstart handler on the document.

                    activeDocHandlers["touchstart"] =
                        (activeDocHandlers["touchstart"] || 0) + 1;

                    if (activeDocHandlers["touchstart"] === 1) {
                        $document
                            .bind("touchstart", handleTouchStart)
                            .bind("touchend", handleTouchEnd)

                            // On touch platforms, touching the screen and then dragging your finger
                            // causes the window content to scroll after some distance threshold is
                            // exceeded. On these platforms, a scroll prevents a click event from being
                            // dispatched, and on some platforms, even the touchend is suppressed. To
                            // mimic the suppression of the click event, we need to watch for a scroll
                            // event. Unfortunately, some platforms like iOS don't dispatch scroll
                            // events until *AFTER* the user lifts their finger (touchend). This means
                            // we need to watch both scroll and touchmove events to figure out whether
                            // or not a scroll happenens before the touchend event is fired.

                            .bind("touchmove", handleTouchMove)
                            .bind("scroll", handleScroll);
                    }
                }
            },

            teardown: function (data, namespace) {
                // If this is the last virtual binding for this eventType,
                // remove its global handler from the document.

                --activeDocHandlers[eventType];

                if (!activeDocHandlers[eventType]) {
                    $document.unbind(realType, mouseEventCallback);
                }

                if (eventCaptureSupported) {
                    // If this is the last virtual mouse binding in existence,
                    // remove our document touchstart listener.

                    --activeDocHandlers["touchstart"];

                    if (!activeDocHandlers["touchstart"]) {
                        $document
                            .unbind("touchstart", handleTouchStart)
                            .unbind("touchmove", handleTouchMove)
                            .unbind("touchend", handleTouchEnd)
                            .unbind("scroll", handleScroll);
                    }
                }

                var $this = $(this),
                    bindings = $.data(this, dataPropertyName);

                // teardown may be called when an element was
                // removed from the DOM. If this is the case,
                // jQuery core may have already stripped the element
                // of any data bindings so we need to check it before
                // using it.
                if (bindings) {
                    bindings[eventType] = false;
                }

                // Unregister the dummy event handler.

                $this.unbind(realType, dummyMouseHandler);

                // If this is the last virtual mouse binding on the
                // element, remove the binding data from the element.

                if (!hasVirtualBindings(this)) {
                    $this.removeData(dataPropertyName);
                }
            },
        };
    }

    // Expose our custom events to the jQuery bind/unbind mechanism.

    for (var i = 0; i < virtualEventNames.length; i++) {
        $.event.special[virtualEventNames[i]] = getSpecialEventObject(
            virtualEventNames[i],
        );
    }

    // Add a capture click handler to block clicks.
    // Note that we require event capture support for this so if the device
    // doesn't support it, we punt for now and rely solely on mouse events.
    if (eventCaptureSupported) {
        document.addEventListener(
            "click",
            function (e) {
                var cnt = clickBlockList.length,
                    target = e.target,
                    x,
                    y,
                    ele,
                    i,
                    o,
                    touchID;

                if (cnt) {
                    x = e.clientX;
                    y = e.clientY;
                    threshold = $.vmouse.clickDistanceThreshold;

                    // The idea here is to run through the clickBlockList to see if
                    // the current click event is in the proximity of one of our
                    // vclick events that had preventDefault() called on it. If we find
                    // one, then we block the click.
                    //
                    // Why do we have to rely on proximity?
                    //
                    // Because the target of the touch event that triggered the vclick
                    // can be different from the target of the click event synthesized
                    // by the browser. The target of a mouse/click event that is syntehsized
                    // from a touch event seems to be implementation specific. For example,
                    // some browsers will fire mouse/click events for a link that is near
                    // a touch event, even though the target of the touchstart/touchend event
                    // says the user touched outside the link. Also, it seems that with most
                    // browsers, the target of the mouse/click event is not calculated until the
                    // time it is dispatched, so if you replace an element that you touched
                    // with another element, the target of the mouse/click will be the new
                    // element underneath that point.
                    //
                    // Aside from proximity, we also check to see if the target and any
                    // of its ancestors were the ones that blocked a click. This is necessary
                    // because of the strange mouse/click target calculation done in the
                    // Android 2.1 browser, where if you click on an element, and there is a
                    // mouse/click handler on one of its ancestors, the target will be the
                    // innermost child of the touched element, even if that child is no where
                    // near the point of touch.

                    ele = target;

                    while (ele) {
                        for (i = 0; i < cnt; i++) {
                            o = clickBlockList[i];
                            touchID = 0;

                            if (
                                (ele === target &&
                                    Math.abs(o.x - x) < threshold &&
                                    Math.abs(o.y - y) < threshold) ||
                                $.data(ele, touchTargetPropertyName) ===
                                    o.touchID
                            ) {
                                // XXX: We may want to consider removing matches from the block list
                                //      instead of waiting for the reset timer to fire.
                                e.preventDefault();
                                e.stopPropagation();
                                return;
                            }
                        }
                        ele = ele.parentNode;
                    }
                }
            },
            true,
        );
    }
})($, window, document);
